/****************************************************************************
* Copyright (c) 2008, j. montgomery, ATGi                                   *
* All rights reserved.                                                      *
*                                                                           *
* Redistribution and use in source and binary forms, with or without        *
* modification, are permitted provided that the following conditions        *
* are met:                                                                  *
*                                                                           *
* - Redistributions of source code must retain the above copyright          *
*   notice, this list of conditions and the following disclaimer.           *
*                                                                           *
* - Redistributions in binary form must reproduce the above                 *
*   copyright notice, this list of conditions and the following             *
*   disclaimer in the documentation and/or other materials provided         *
*   with the distribution.                                                  *
*                                                                           *
* - Neither the name of ATGi nor the names of its contributors may          *
*   be used to endorse or promote products derived from this software       *
*   without specific prior written permission.                              *
*                                                                           *
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS       *
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT         *
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS         *
* FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE            *
* COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,       *
* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES                  *
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR        *
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)        *
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,       *
* STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)             *
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED       *
* OF THE POSSIBILITY OF SUCH DAMAGE.                                        *
*****************************************************************************
*/
using System;
using System.Collections;
using System.ComponentModel;
using System.Globalization;
using System.IO;
using System.Text;
using System.Text.RegularExpressions;

using DnSmtp.Message.Encoding;


namespace DnSmtp.Message
{
	/// <summary>
	/// Summary description for MailMessage.
	/// </summary>
	public class MailMessage : ICloneable
	{
		#region Constants
		private const string DATE_HEADER = "Date: ";
		private const string TO_HEADER = "To: ";
		private const string CC_HEADER = "Cc: ";
		private const string FROM_HEADER = "From: ";
		private const string REPLYTO_HEADER = "Reply-To: ";
		private const string SUBJECT_HEADER = "Subject: ";
		private const string RETURN_HEADER = "Return-Path: ";
		private const string MIMEVERSION_HEADER = "MIME-Version: 1.0";
		private const string XMAILER_HEADER = "X-Mailer: ";
		internal static readonly string FILENAME_HEADER = "name=";
		internal static readonly string CONTENT_TYPE_HEADER = "Content-Type: ";
		internal static readonly string BOUNDRY_HEADER = "boundary=";
		internal static readonly string CHARSET_USASCII_HEADER = "charset=us-ascii;";
		internal static readonly string CRLF = "\r\n";
		#endregion Constants

		#region Fields
		private EmailAddress _maskedTo;
		private EmailAddress _fromAddress;
		private EmailAddress _replyTo;
		private EmailAddressList _toAddresses;
		private EmailAddressList _ccAddresses;
		private EmailAddressList _bccAddresses;
		private Stack _synchRecipientStack;
		private string _subject;
		private MessageContentCollection _messageCollection;
		private bool _isMessageReady = false;
		#endregion Fields

		#region Properties
		public EmailAddress MaskedToAddress 
		{
			get { return _maskedTo; }
			set
			{
				if(!_maskedTo.Equals(value)) 
				{
					if(PropertyChangedEvent != null) 
					{
						PropertyChangedEvent(this, new PropertyChangedEventArgs("MaskedToAddress"));
					}
				}
				_maskedTo = value;
			}
		}

		public EmailAddress ReplyTo 
		{
			get { return _replyTo; }
			set
			{
				if(!_replyTo.Equals(value)) 
				{
					if(PropertyChangedEvent != null) 
					{
						PropertyChangedEvent(this, new PropertyChangedEventArgs("ReplyTo"));
					}
				}
				_replyTo = value;
			}
		}

		public EmailAddress FromAddress 
		{
			get { return _fromAddress; }
			set 
			{
				if(!_fromAddress.Equals(value))
				{
					if(PropertyChangedEvent != null) 
					{
						PropertyChangedEvent(this, new PropertyChangedEventArgs("FromAddress"));
					}
				}
				_fromAddress = value;
			}
		}

		public string Subject 
		{
			get { return _subject; }
			set { _subject = value; }
		}

		public EmailAddressList ToAddresses
		{
			get
			{
				return _toAddresses;
			}
			set
			{
				if (_toAddresses != value)
				{
					if (PropertyChangedEvent != null)
					{
						PropertyChangedEvent(this, new PropertyChangedEventArgs("ToAddresses"));
					}
				}
				_toAddresses = value;
			}
		}

		public EmailAddressList CcAddresses 
		{
			get { return _ccAddresses; }
			set
			{
				if(_ccAddresses != value) 
				{
					if(PropertyChangedEvent != null) 
					{
						PropertyChangedEvent(this, new PropertyChangedEventArgs("CcAddresses"));
					}
				}
				_ccAddresses = value;
			}
		}

		public EmailAddressList BccAddresses
		{
			get
			{
				return _bccAddresses;
			}
			set
			{
				if (_bccAddresses != value)
				{
					if (PropertyChangedEvent != null)
					{
						PropertyChangedEvent(this, new PropertyChangedEventArgs("BccAddresses"));
					}
				}
				_bccAddresses = value;
			}
		}

		public MessageContentCollection Items 
		{
			get { return _messageCollection; }
			set
			{
				if(_messageCollection != value) 
				{
					if(PropertyChangedEvent != null) 
					{
						PropertyChangedEvent(this, new PropertyChangedEventArgs("Items"));
					}
				}
				_messageCollection = value;
			}
		}

		public Stack SynchRecipientStack 
		{
			get
			{
				if (_synchRecipientStack == null)
				{
					BuildRecipientAddressStack();
				}
				return _synchRecipientStack;
			}
			set { _synchRecipientStack = value; }
		}
		#endregion Properties

		#region Constructors
		public MailMessage()
		{
			_toAddresses = new EmailAddressList();
			_ccAddresses = new EmailAddressList();
			_bccAddresses = new EmailAddressList();
			_messageCollection = new MessageContentCollection();
		}

		public MailMessage(string to, string from, string subject) : this()
		{
			_fromAddress = new EmailAddress(from);
			_toAddresses.Add(to);
			_subject = subject;
		}

		public MailMessage(string to, string from, string subject, string textMessageBody) : this()
		{
			_fromAddress = new EmailAddress(from);
			_toAddresses.Add(to);
			_subject = subject;
			this.Items.Add(new MessageContent(MimeType.TextPlain, textMessageBody));
		}
		#endregion Constructors

		public PropertyChangedEventHandler PropertyChangedEvent;

		private void BuildRecipientAddressStack()
		{
			ArrayList allRcptEmail = new ArrayList(_toAddresses.Count + _ccAddresses.Count + _bccAddresses.Count);
			int toIndex = 0;
			int ccIndex = (_toAddresses.Count > 0) ? _toAddresses.Count - 1 : 0;
			int bccIndex = (ccIndex > 0) ? _ccAddresses.Count - 1 : _toAddresses.Count - 1;
			// Retrieve and combine the total recipient list and store them in a stack for delivery

			// TODO: Make sure these don't leave empty items in the array
			allRcptEmail.InsertRange(toIndex, _toAddresses);
			if(_ccAddresses.Count > 0)
				allRcptEmail.InsertRange(ccIndex, _ccAddresses);
			if(_bccAddresses.Count > 0)
				allRcptEmail.InsertRange(bccIndex, _bccAddresses);
			_synchRecipientStack = Stack.Synchronized(new Stack(allRcptEmail));
		}

		internal protected static string GenerateMailBoundary() 
		{
			Guid mailBoundry = Guid.NewGuid();
			return mailBoundry.ToString().Replace("-", string.Empty);
		}

		/// <summary>
		/// Returns a string representation of the mail message, headers and all
		/// </summary>
		/// <returns></returns>
		public override string ToString()
		{
			return this.BuildMessage();
		}

		/// <summary>
		/// 
		/// </summary>
		/// <returns></returns>
		internal string BuildMessage()
		{
			StringBuilder sb = new StringBuilder();
			sb.Append(this.BuildHeader());
			sb.Append(this.Items.BuildMessage());
			return sb.ToString();
		}

		private string BuildHeader() 
		{
			StringBuilder sb = new StringBuilder();
			sb.Append(MIMEVERSION_HEADER);
			sb.Append(MailMessage.CRLF);
			sb.Append(DATE_HEADER);
			sb.Append(this.BuildDateHeader());
			sb.Append(MailMessage.CRLF);
			sb.Append(FROM_HEADER);
			sb.Append(FromAddress);
			sb.Append(MailMessage.CRLF);
			sb.Append(TO_HEADER + BuildAddressList(_toAddresses));
			sb.Append(MailMessage.CRLF);
			// Add X-Mailer: header
			sb.Append(XMAILER_HEADER);
			sb.Append("DnSmtp Mailer (v");
			sb.Append(System.Environment.Version.Major);
			sb.Append(".");
			sb.Append(System.Environment.Version.Minor);
			sb.Append(".");
			sb.Append(System.Environment.Version.Revision);
			sb.Append(" Build ");
			sb.Append(System.Environment.Version.Build);
			sb.Append(")");
			sb.Append(MailMessage.CRLF);

			if (_ccAddresses.Count > 0)
			{
				sb.Append(CC_HEADER);
				sb.Append(BuildAddressList(_ccAddresses));
				sb.Append(MailMessage.CRLF);
			}

			if (_replyTo.Email != null && _replyTo.Email.Length > 0)
			{
				sb.Append(REPLYTO_HEADER);
				sb.Append(ReplyTo.ToString());
				sb.Append(MailMessage.CRLF);
			}

			sb.Append(SUBJECT_HEADER);
			sb.Append(_subject);
			sb.Append(MailMessage.CRLF);
			return sb.ToString();
		}

		/// <summary>
		/// 
		/// </summary>
		/// <param name="addressList"></param>
		/// <returns></returns>
		internal static string BuildAddressList(EmailAddressList addressList) 
		{
			if(addressList == null)
				throw new ArgumentNullException();

			StringBuilder sb = new StringBuilder();
			
			foreach(EmailAddress address in addressList) 
			{
				sb.Append(address.ToString());
                sb.Append(";");
			}

			// return the string minus the extra ';' at the end
			return sb.ToString().TrimEnd(new char[] {';'});
		}

		private string BuildDateHeader() 
		{
			DateTime dtNow = DateTime.Now;
			// See http://cr.yp.to/immhf/date.html for details
			// next 3 lines calc the timezone offset from GMT, e.g. EST would be -0500, GMT would be -0000
			string timeZone = TimeZone.CurrentTimeZone.GetUtcOffset(dtNow).ToString(); // Use current Time Zone
			timeZone = timeZone.Replace(":","");
			timeZone = timeZone.Remove(timeZone.Length-2,2);
			return dtNow.ToString("ddd, dd MMM yyyy hh:mm:ss", DateTimeFormatInfo.InvariantInfo) + " " + timeZone;
		}

		public void AddHtmlMessage(string message) 
		{
			AddMessageContent(message, MimeType.TextHtml, false);
		}

		public void AddTextMessage(string message) 
		{
			AddMessageContent(message, MimeType.TextPlain, false);
		}

		public void AddAolMessage(string message)
		{ 
			AddMessageContent(message, MimeType.TextXAol, false);
		}

		public void AddAttachment(string fullPath) 
		{
			if (fullPath == null)
				throw new ArgumentNullException("fullPath");	

			FileInfo f = new FileInfo(fullPath);
			
			if(!f.Exists)
				throw new FileNotFoundException();
			Stream sr = f.Open(FileMode.Open, FileAccess.Read, FileShare.Read);

			AddAttachment(sr, f.Name);
			// Don't close stream here or we can't read it when we actually encode the message!
		}

		public void AddAttachment(Stream stream, string fileName) 
		{
			if (stream == null)
				throw new ArgumentNullException("stream");
			
			if(fileName == null) 
				throw new ArgumentNullException("fileName");

			AttachmentContent attachContent = new AttachmentContent();
			
			// Retrieve the extension
			FileInfo fi = new FileInfo(fileName);
			
			attachContent.MimeType = MimeType.GetMimeTypeByExtension(fi.Extension);

			attachContent.EncodingStrategy = Base64EncodingStrategy.Instance;

			attachContent.MessageBody = stream;
			attachContent.FileName = fileName;

			AddMessageContent(attachContent, true);
		}

		private void AddMessageContent(string message, MimeType type, bool isAttachment) 
		{
			// Prepare the messageContent to be added.
			MessageContent messageContent = new MessageContent(type, message);
			messageContent.EncodingStrategy = type.PreferedEncodingStrategy;
			AddMessageContent(messageContent, isAttachment);			
		}

		private void AddMessageContent(IMailContent message, bool isAttachment) 
		{
			if(!isAttachment)
			{
				// If the Items is of type "multi/alternative"
				if(Items.MimeType.Equals(MimeType.MultipartAlternative))
				{
					// Add the content to the Items
					Items.Add(message);
					return;
				}
				else if(!Items.MimeType.Equals(MimeType.MultipartAlternative)) // Not a multipart/alternative
				{
					bool foundOne = false;
					foreach(IMailContent content in Items) 
					{
						if(content is MessageContentCollection) 
						{
							if(content.MimeType.Equals(MimeType.MultipartAlternative))
							{
								foundOne = true;
								// Add the content to the found collection.
								((MessageContentCollection)content).Add(message);
								return;
							}
						}
					}
					// Didn't find a multipart/alternative so we add one
					if(!foundOne) 
					{
						MessageContentCollection mcc = new MessageContentCollection();
						mcc.MimeType = MimeType.MultipartAlternative;
						// Add the content to the created collection.
						mcc.Add(message);
						// Add a collection of type "multi/alternative" to the Items collection.	
						Items.Add(mcc);
						return;
					}
				}
				System.Diagnostics.Debug.Assert(false, "Mimetype was not Multipart/Mixed or Multipart/Alternative - this should be impossible.");
			}
			else // adding an attachment
			{
				// If the Items is of type "multi/mixed"
				if(Items.MimeType.Equals(MimeType.MultipartMixed)) 
				{
					// Add the content to the items.
					Items.Add(message);
				}
				
				else // Items is of type "multi/alternative"
				{
					// Are there any items?
					if(Items.Count == 0) 
					{
						// No, just change the type and add
						Items.MimeType = MimeType.MultipartMixed;
						Items.Add(message);
					}
					else // There is an item
					{
						// Copy the alternative tree into a child, attach, etc.
						MessageContentCollection mcc = new MessageContentCollection();
						mcc = (MessageContentCollection)Items.Clone();
					
						// Change the type of Items to "multi/mixed"
						Items.MimeType = MimeType.MultipartMixed;
						Items.Clear();
						Items.Add(mcc);
						// Add the content to the Items.
						Items.Add(message);
					}
				}
			}
		}

		internal static string FormatStringWidth(string s, int maxLen)
		{
			if (s == null)
				throw new ArgumentNullException();

			StringBuilder sb = new StringBuilder();
			StringWriter sw = new StringWriter();

			try
			{
				int i=0;
				
				foreach(char c in s) 
				{
					sb.Append(c.ToString());
					i++;
					if (i==maxLen)
					{
						sw.Write(sb.ToString() + MailMessage.CRLF);
						sw.Flush();

						i=0;
						sb = new StringBuilder();
					}
				}
				
				sw.WriteLine(sb.ToString());
				sw.Flush();

				return sw.ToString();
			}
			finally
			{
				sw.Close();
			}
		}

	

		#region ICloneable Members
		public object Clone()
		{
			MailMessage mm = new MailMessage();
			mm._bccAddresses = this._bccAddresses;
			mm._ccAddresses = this._ccAddresses;
			mm._fromAddress = this._fromAddress;
			mm._isMessageReady = this._isMessageReady;
			mm._maskedTo = this._maskedTo;
			mm._messageCollection = this._messageCollection;
			mm._subject = this._subject;
			// Don't clone these pieces
			mm._synchRecipientStack = new Stack();
			mm._toAddresses = new EmailAddressList();
			return mm;
		}
		#endregion
	}
}
